<?php

class Parser {
	private $reader = null;
	private $inside_item = array();                  // Keep track of current position in tag tree
	private $current_tag = null;                     // Last entered tag name
	private $current_attr = null;                     // Attributes array of last entered tag
	private $namespaces = array(
		'http://purl.org/rss/1.0/'                  => 'RSS 1.0',
		'http://purl.org/rss/1.0/modules/content/'  => 'RSS 2.0',
		'http://www.w3.org/2005/Atom'               => 'ATOM 1',
	); // Namespaces to detact feed version
	private $item_tags = array('ITEM','ENTRY');    // List of tag names which holds a feed item
	private $channel_tags = array('CHANNEL','FEED');  // List of tag names which holds all channel elements
	private $date_tags = array('UPDATED','PUBDATE','DC:DATE');
	private $has_sub_tags = array('IMAGE','AUTHOR');  // List of tag names which have sub tags
	private $channels = array();
	private $items = array();
	private $item_index = 0;
	private $url = null; // The parsed url
	private $version = null; // Detected feed version

	function __construct() {
		$this->reader = xml_parser_create();
		xml_set_object($this->reader, $this);
		xml_set_element_handler($this->reader, "start", "end");
		xml_set_character_data_handler($this->reader, "characterData");
	}

	private function getUrlContent() {
		if(empty($this->url)) {
			die(false);
		}
		if($content = @file_get_contents($this->url)) {
			return $content;
		} else {
			$ch         = curl_init();
			curl_setopt($ch, CURLOPT_URL, $this->url);
			curl_setopt($ch, CURLOPT_HEADER, false);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			$content    = curl_exec($ch);
			$error      = curl_error($ch);
			curl_close($ch);
			if(empty($error)) {
				return $content;
			} else {
				die(false);
			}
		}
	}

	private function findVersion($tag_name, $attrs) {
		$namespace = array_values($attrs);
		foreach($this->namespaces as $value =>$version) {
			if(in_array($value, $namespace)) {
				$this->version = $version;
				return;
			}
		}
	}

	private function getParentTag() {
		return $this->inside_item[count($this->inside_item) - 2];
	}

	private function start($parser, $tag_name, $attrs) {
		if(!$this->version) {
			$this->findVersion($tag_name, $attrs);
		}
		array_push($this->inside_item, $tag_name);
		$this->current_tag  = $tag_name;
		$this->current_attr = $attrs;
	}

	private function end($parser, $tag_name) {
		if (in_array($tag_name, $this->item_tags)) {
			$this->item_index++;
		}
		array_pop($this->inside_item);
		$this->current_tag = $this->inside_item[count($this->inside_item)-1];
	}

	private function inChannel() {
		if($this->version == 'RSS 1.0') {
			if(in_array('CHANNEL', $this->inside_item) && $this->current_tag != 'CHANNEL')
			return TRUE;
		} else if($this->version == 'RSS 2.0') {
			if(in_array('CHANNEL', $this->inside_item) && !in_array('ITEM', $this->inside_item) && $this->current_tag != 'CHANNEL')
			return TRUE;
		} else if($this->version == 'ATOM 1') {
			if(in_array('FEED', $this->inside_item) && !in_array('ENTRY', $this->inside_item) && $this->current_tag != 'FEED')
			return TRUE;
		}
		return FALSE;
	}

	private function inItem() {
		if($this->version == 'RSS 1.0' || $this->version == 'RSS 2.0') {
			if(in_array('ITEM', $this->inside_item) && $this->current_tag != 'ITEM')
			return TRUE;
		} else if($this->version == 'ATOM 1') {
			if(in_array('ENTRY', $this->inside_item) && $this->current_tag != 'ENTRY')
			return TRUE;
		}
		return FALSE;
	}

	private function unhtmlentities($string) {
		// Get HTML entities table
		$trans_tbl = get_html_translation_table (HTML_ENTITIES, ENT_QUOTES);
		// Flip keys<==>values
		$trans_tbl = array_flip ($trans_tbl);
		// Add support for &apos; entity (missing in HTML_ENTITIES)
		$trans_tbl += array('&apos;' => "'");
		// Replace entities by values
		return strtr ($string, $trans_tbl);
	}

	public function getTotalItems() {
		return count($this->items);
	}

	public function getItems() {
		return $this->items;
	}

	public function getItem($index) {
		if($index < $this->getTotalItems()) {
			return $this->items[$index];
		} else {
			die(false);
		}
	}

	public function getChannels() {
		return $this->channels;
	}

	public function getChannel($tag_name) {
		if(array_key_exists(strtoupper($tag_name), $this->channels)) {
			return $this->channels[strtoupper($tag_name)];
		} else {
			die(false);
		}
	}

	public function getFeedVersion() {
		return $this->version;
	}

	public function getParsedUrl() {
		if(empty($this->url)) {
			return FALSE;
		} else {
			return $this->url;
		}
	}

	public function parse($url) {
		$this->url  = $url;
		$content = $this->getUrlContent();
		if($content) {
			$segments   = str_split($content, 4096);
			foreach($segments as $index=>$data) {
				$lastPiece = ((count($segments)-1) == $index)? true : false;
				xml_parse($this->reader, $data, $lastPiece)
				or die(false);
			}
			xml_parser_free($this->reader);
		} else {
			die(false);
		}
		if(empty($this->version)) {
			die(false);
		}
	}

	private function characterData($parser, $data) {
		//Converting all date formats to timestamp
		if(in_array($this->current_tag, $this->date_tags)) {
			$data = strtotime($data);
		}
			
		if($this->inChannel()) {
			// If has subtag, make current element an array and assign subtags as it's element
			if(in_array($this->getParentTag(), $this->has_sub_tags)) {
				if(! is_array($this->channels[$this->getParentTag()])) {
					$this->channels[$this->getParentTag()] = array();
				}
				$this->channels[$this->getParentTag()][$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				return;
			} else {
				if(! in_array($this->current_tag, $this->has_sub_tags)) {
					$this->channels[$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				}
			}
			if(!empty($this->current_attr)) {
				$this->channels[$this->current_tag . '_ATTRS'] = $this->current_attr;

				//If the tag has no value
				if(strlen($this->channels[$this->current_tag]) < 2) {
					//If there is only one attribute, assign the attribute value as channel value
					if(count($this->current_attr) == 1) {
						foreach($this->current_attr as $attr_val) {
							$this->channels[$this->current_tag] = $attr_val;
						}
					}
					//If there are multiple attributes, assign the attributs array as channel value
					else {
						$this->channels[$this->current_tag] = $this->current_attr;
					}
				}
			}
		} else if($this->inItem()) {
			// If has subtag, make current element an array and assign subtags as it's elements
			if(in_array($this->getParentTag(), $this->has_sub_tags)) {
				if(! is_array($this->items[$this->item_index][$this->getParentTag()])) {
					$this->items[$this->item_index][$this->getParentTag()] = array();
				}

				$this->items[$this->item_index][$this->getParentTag()][$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				return;
			} else {
				if(! in_array($this->current_tag, $this->has_sub_tags)) {
					$this->items[$this->item_index][$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				}
			}

			if(!empty($this->current_attr)) {
				$this->items[$this->item_index][$this->current_tag . '_ATTRS'] = $this->current_attr;

				//If the tag has no value
				if(strlen($this->items[$this->item_index][$this->current_tag]) < 2) {
					//If there is only one attribute, assign the attribute value as feed element's value
					if(count($this->current_attr) == 1) {
						foreach($this->current_attr as $attr_val) {
							$this->items[$this->item_index][$this->current_tag] = $attr_val;
						}
					} else { //If there are multiple attributes, assign the attribute array as feed element's value
						$this->items[$this->item_index][$this->current_tag] = $this->current_attr;
					}
				}
			}
		}
	}
}